import math
import os
import time
from enum import Enum
from ui.ui_widget import Ui_Widget
from worker.worker import Worker
from utils.border import get_border
from utils.intersection import get_intersection
from utils.fit import get_tangent
from PySide6.QtCore import Signal, QRect
from PySide6.QtGui import QPainter, QColor
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout, QFileDialog


# 工具运行时错误
class RunningError(Exception):
    def __init__(self, msg):
        self.msg = msg


# 错误处理装饰器
def running_error(func):
    def append_error_log(self, *args, **kwargs):
        try:
            func(self, *args, **kwargs)
        except RunningError as e:
            self.append_log_sig.emit(e.msg)
    return append_error_log


# 处理模式
class Mode(Enum):
    SINGLE_MODE = 0  # 单张模式
    MULTI_MODE = 1  # 多张模式


# 显示图像（已按照ImageWidget窗口大小进行处理）
class Image:
    border = []  # 边界点
    horizontal_line = []  # 水平线
    tangent_line = []  # 切线
    vertical_line = []  # 垂线
    target_point1 = None  # 与水平线交点
    target_point2 = None  # 与垂线交点
    fit_window = None  # 拟合窗


# 图片展示窗口
class ImageWidget(QWidget):
    disable_sig = Signal(bool)

    def __init__(self):
        super(ImageWidget, self).__init__()
        self.disabled = True  # ImageWidget禁用状态
        self.disable_sig.connect(self.set_disabled)

    def set_disabled(self, disabled):
        self.disabled = disabled
        self.update()

    def paintEvent(self, event):
        painter = QPainter()
        painter.begin(self)
        painter.fillRect(QRect(0, 0, self.width(), self.height()), QColor(255, 255, 255))
        if not self.disabled:
            def get_point(center, r):
                return center[0] - r, center[1] - r
            # 绘制边界点
            painter.setPen(QColor(0, 255, 0))
            for x, y in Image.border:
                painter.drawArc(*get_point((x, y), 0.5), 1, 1, 0, 360 * 16)
            # 绘制水平线
            painter.setPen(QColor(255, 0, 0))
            painter.drawLine(*Image.horizontal_line[0], *Image.horizontal_line[1])
            # 绘制水平线与曲线交点
            if Image.target_point1:
                painter.setPen(QColor(0, 0, 0))
                painter.drawArc(*get_point(Image.target_point1, 3), 6, 6, 0, 360 * 16)
            # 绘制拟合窗
            if Image.fit_window:
                painter.setPen(QColor(255, 0, 255))
                painter.drawRect(QRect(*Image.fit_window))
            # 绘制切线
            if Image.tangent_line:
                painter.setPen(QColor(0, 0, 255))
                painter.drawLine(*Image.tangent_line[0], *Image.tangent_line[1])
            # 绘制垂线
            if Image.vertical_line:
                painter.setPen(QColor(255, 255, 0))
                painter.drawLine(*Image.vertical_line[0], *Image.vertical_line[1])
            # 绘制水平线与曲线交点
            if Image.target_point2:
                painter.setPen(QColor(0, 0, 0))
                painter.drawArc(*get_point(Image.target_point2, 3), 6, 6, 0, 360 * 16)
        painter.end()


class Widget(QWidget):
    append_log_sig = Signal(str)  # 添加日志信号

    def __init__(self):
        # 常量
        self.std_height = 7
        # 参数
        self.image_path = ''
        self.output_path = ''
        self.mode = Mode.SINGLE_MODE
        self.first_number = 0
        self.last_number = 0
        self.curr_number = 0
        self.horizontal_line = 0
        self.top = 0
        self.bottom = 0
        self.left = 0
        self.right = 0
        self.poly_num = 0
        # 建立UI
        super(Widget, self).__init__()
        self.ui = Ui_Widget()
        self.ui.setupUi(self)
        # 创建子线程
        self.worker = Worker()
        self.worker.progress_sig.connect(lambda value: self.ui.outputProgressBar.setValue(value))
        self.worker.finish_sig.connect(self.output_finish)
        # 绑定日志信号槽
        self.append_log_sig.connect(lambda log: self.ui.logTextEdit.append('[{}]{}'.format(time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()), log)))
        # 添加image_widget
        layout = QVBoxLayout()
        layout.addWidget(ImageWidget())
        self.ui.imageWidget.setLayout(layout)
        # 绑定控件信号
        self.ui.clearLogButton.clicked.connect(lambda: self.ui.logTextEdit.clear())
        self.ui.imagePathButton.clicked.connect(self.select_image_path)
        self.ui.outputPathButton.clicked.connect(self.select_output_path)
        self.ui.singleModeRadioButton.clicked.connect(lambda: self.ui.lastNumberSpinBox.setDisabled(True))
        self.ui.multiModeRadioButton.clicked.connect(lambda: self.ui.lastNumberSpinBox.setDisabled(False))
        self.ui.runButton.clicked.connect(self.run)
        self.ui.stopButton.clicked.connect(self.stop)
        self.ui.applyFitArgsButton.clicked.connect(self.apply_fit_args)
        self.ui.previousImageButton.clicked.connect(self.previous_image)
        self.ui.nextImageButton.clicked.connect(self.next_image)
        self.ui.jumpImageButton.clicked.connect(self.jump_image)
        self.ui.outputButton.clicked.connect(lambda: self.output(self.first_number, self.last_number))
        self.ui.outputCurrImageButton.clicked.connect(lambda: self.output(self.curr_number, self.curr_number))
        # 初始状态
        self.stop()

    # 设置config的开关
    def set_config_disabled(self, disabled):
        self.ui.imagePathButton.setDisabled(disabled)
        self.ui.outputPathButton.setDisabled(disabled)
        self.ui.previousNameLineEdit.setDisabled(disabled)
        self.ui.lastNameLineEdit.setDisabled(disabled)
        self.ui.singleModeRadioButton.setDisabled(disabled)
        self.ui.multiModeRadioButton.setDisabled(disabled)
        self.ui.firstNumberSpinBox.setDisabled(disabled)
        self.ui.lastNumberSpinBox.setDisabled(self.ui.singleModeRadioButton.isChecked() or disabled)
        self.ui.horizontalLineDoubleSpinBox.setDisabled(disabled)

    # 设置console的开关
    def set_console_state(self, run=True):
        self.ui.runButton.setDisabled(not run)
        self.ui.stopButton.setDisabled(run)

    # 设置fit的开关
    def set_fit_disabled(self, disabled):
        self.ui.topDoubleSpinBox.setDisabled(disabled)
        self.ui.bottomDoubleSpinBox.setDisabled(disabled)
        self.ui.leftDoubleSpinBox.setDisabled(disabled)
        self.ui.rightDoubleSpinBox.setDisabled(disabled)
        self.ui.fitFunctionComboBox.setDisabled(disabled)
        self.ui.applyFitArgsButton.setDisabled(disabled)

    # 设置output的开关
    def set_output_disabled(self, disabled):
        self.ui.outputButton.setDisabled(disabled)
        self.ui.outputCurrImageButton.setDisabled(disabled)

    # 设置show_widget的开关
    def set_show_widget_disabled(self, disabled):
        if disabled:
            self.ui.realtimeAngleLabel.setText('None')
            self.ui.realtimeDistanceLabel.setText('None')
        self.ui.previousImageButton.setDisabled(disabled or self.mode == Mode.SINGLE_MODE)
        self.ui.nextImageButton.setDisabled(disabled or self.mode == Mode.SINGLE_MODE)
        self.ui.jumpImageButton.setDisabled(disabled or self.mode == Mode.SINGLE_MODE)
        self.ui.jumpNumberSpinBox.setDisabled(disabled or self.mode == Mode.SINGLE_MODE)
        self._image_widget().disable_sig.emit(disabled)

    # 选择图片路径
    def select_image_path(self):
        image_path = QFileDialog.getExistingDirectory(self, '选择图片所在文件夹')
        if image_path:
            self.ui.imagePathLineEdit.setText(image_path)

    # 选择输出路径
    def select_output_path(self):
        output_path = QFileDialog.getExistingDirectory(self, '选择输出文件夹')
        if output_path:
            self.ui.outputPathLineEdit.setText(output_path)

    # 运行程序
    @running_error
    def run(self):
        # 先进行一些有效性检查
        if self.ui.imagePathLineEdit.text() == '':
            raise RunningError('【错误】未选择图片路径！')
        if self.ui.outputPathLineEdit.text() == '':
            raise RunningError('【错误】未选择输出路径！')
        if self.ui.lastNameLineEdit.text() == '':
            raise RunningError('【错误】未输入图片后缀名！')
        first_number = self.ui.firstNumberSpinBox.value()
        last_number = first_number if self.ui.singleModeRadioButton.isChecked() else self.ui.lastNumberSpinBox.value()
        if first_number > last_number:
            raise RunningError('【错误】无效的图片序号范围：{}-{}'.format(first_number, last_number))
        for i in range(first_number, last_number + 1):
            curr_path = os.path.join(self.ui.imagePathLineEdit.text(), self._get_image_name(i))
            if not os.path.exists(curr_path):
                raise RunningError('【错误】{}不存在！'.format(curr_path))
        # 检查通过，保存配置
        self.image_path = self.ui.imagePathLineEdit.text()
        self.output_path = self.ui.outputPathLineEdit.text()
        self.mode = Mode.SINGLE_MODE if self.ui.singleModeRadioButton.isChecked() else Mode.MULTI_MODE
        self.first_number = first_number
        self.last_number = last_number
        self.curr_number = first_number
        self.horizontal_line = self.ui.horizontalLineDoubleSpinBox.value()
        # 打开或关闭控件
        self._set_tool_state(False)
        # 绘制第一帧图像
        self.apply_fit_args()

    # 停止运行
    def stop(self):
        self._set_tool_state(True)

    # 应用拟合参数
    def apply_fit_args(self):
        self.top = self.ui.topDoubleSpinBox.value()
        self.bottom = self.ui.bottomDoubleSpinBox.value()
        self.left = self.ui.leftDoubleSpinBox.value()
        self.right = self.ui.rightDoubleSpinBox.value()
        self.poly_num = self.ui.fitFunctionComboBox.currentIndex() + 1
        self._draw_image(self._get_image_name(self.curr_number))

    # 切换到上一张图片
    def previous_image(self):
        if self.curr_number > self.first_number:
            self.curr_number -= 1
            self._draw_image(self._get_image_name(self.curr_number))

    # 切换到下一张图片
    def next_image(self):
        if self.curr_number < self.last_number:
            self.curr_number += 1
            self._draw_image(self._get_image_name(self.curr_number))

    # 跳到指定图片
    @running_error
    def jump_image(self):
        index = self.ui.jumpNumberSpinBox.value()
        if self.first_number <= index <= self.last_number:
            self.curr_number = index
            self._draw_image(self._get_image_name(self.curr_number))
        else:
            raise RunningError('【错误】无效的图片序号！')

    # 一键导出Excel+图片
    def output(self, fn, ln):
        # 禁用
        self.ui.stopButton.setDisabled(True)
        self.set_fit_disabled(True)
        self.set_output_disabled(True)
        # 设置待处理的图片名
        img_names = []
        for i in range(fn, ln + 1):
            curr_path = os.path.join(self.image_path, self._get_image_name(i))
            img_names.append(curr_path)
        self.worker.set_args(img_names, self.output_path, (fn, ln), self.horizontal_line,
                             (self.top, self.bottom, self.left, self.right, self.poly_num))
        # 开启子线程
        self.worker.start()

    # 导出完成
    def output_finish(self):
        # 解除禁用
        self.ui.stopButton.setDisabled(False)
        self.set_fit_disabled(False)
        self.set_output_disabled(False)

    # 设置程序运行状态
    def _set_tool_state(self, run=True):
        self.set_config_disabled(not run)
        self.set_console_state(run)
        self.set_fit_disabled(run)
        self.set_output_disabled(run)
        self.set_show_widget_disabled(run)

    # 获得图片名
    def _get_image_name(self, index):
        return '{}{}{}.{}'.format(self.ui.previousNameLineEdit.text(), ''.join(['0', '0', '0'][0:3 - len(str(index))]),
                                  index, self.ui.lastNameLineEdit.text())

    # 获取image_widget
    def _image_widget(self):
        return self.ui.imageWidget.layout().itemAt(0).widget()

    # 绘制图片
    def _draw_image(self, image_name):
        self.ui.realtimeAngleLabel.setText('None')
        self.ui.realtimeDistanceLabel.setText('None')
        self.ui.imageNameLabel.setText(image_name)
        image_name = os.path.join(self.image_path, image_name)
        # 所有计算按照原始图像尺寸进行，填入Image时再进行修改和缩放
        border_x, border_y, img_w, img_h = get_border(image_name, False)
        # 缩放倍数
        scale1 = self._image_widget().height() / img_h
        scale2 = self._image_widget().height() / self.std_height
        scale3 = img_h / self.std_height
        # 计算边界点
        Image.border = list(zip(border_x * scale1, self._image_widget().height() - border_y * scale1))
        # 计算水平线
        Image.horizontal_line = [(0, self._image_widget().height() - self.horizontal_line * scale2),
                                 (self._image_widget().width(), self._image_widget().height() - self.horizontal_line * scale2)]
        # 计算水平线与曲线的交点
        intersections = get_intersection(zip(border_x, border_y), 0, self.horizontal_line * scale3)
        if len(intersections) >= 1:
            if len(intersections) >= 2:
                x, y = intersections[1]
            else:
                x, y = intersections[0]
            Image.target_point1 = (x * scale1, self._image_widget().height() - y * scale1)
            # 计算拟合窗
            Image.fit_window = (
                Image.target_point1[0] - self.left * scale2, Image.target_point1[1] - self.top * scale2,
                (self.left + self.right) * scale2, (self.top + self.bottom) * scale2,
            )
            # 计算切线
            k, b = get_tangent(zip(border_x, border_y), (x, y),
                               (self.top * scale3, self.bottom * scale3, self.left * scale3, self.right * scale3), self.poly_num)
            Image.tangent_line = [
                (0, self._image_widget().height() - b * scale1),
                (img_w * scale1, self._image_widget().height() - (k * img_w + b) * scale1)
            ]
            # 切线不可能90度或0度
            # 计算垂线
            kk = -1 / k
            bb = y - kk * x
            Image.vertical_line = [
                (0, self._image_widget().height() - bb * scale1),
                (img_w * scale1, self._image_widget().height() - (kk * img_w + bb) * scale1)
            ]
            # 计算垂线与曲线的交点
            _intersections = get_intersection(zip(border_x, border_y), kk, bb)
            if len(_intersections) >= 2:
                _x, _y = _intersections[0]
                if math.fabs(_x - x) < 3 and math.fabs(_y - y) < 3:  # 修正
                    _x, _y = _intersections[1]
                Image.target_point2 = (_x * scale1, self._image_widget().height() - _y * scale1)
                # 实时显示角度和厚度
                self.ui.realtimeAngleLabel.setText(str(math.atan2(kk, 1) * 180 / math.pi))
                self.ui.realtimeDistanceLabel.setText('{}  cm'.format(math.dist((x, y), (_x, _y)) / scale3))
            else:
                Image.target_point2 = None
        else:
            Image.target_point1 = None
            Image.target_point2 = None
            Image.fit_window = None
            Image.tangent_line = []
            Image.vertical_line = []
        # 调用ImageWidget的update
        self._image_widget().disable_sig.emit(False)


if __name__ == '__main__':
    import sys
    app = QApplication(sys.argv)
    window = Widget()
    window.setFixedSize(window.width(), window.height())
    window.show()
    sys.exit(app.exec())
